/*
 * Copyright (c) 2020 - present, Inspur Genersoft Co., Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package com.inspur.edp.lcm.metadata.core.index;

import com.inspur.edp.lcm.metadata.api.entity.GspMetadata;
import com.inspur.edp.lcm.metadata.api.entity.Metadata4Ref;
import com.inspur.edp.lcm.metadata.api.entity.MetadataCustomizationFilter;
import com.inspur.edp.lcm.metadata.api.entity.MetadataPackage;
import com.inspur.edp.lcm.metadata.api.entity.MetadataProject;
import com.inspur.edp.lcm.metadata.api.entity.ProjectHeader;
import com.inspur.edp.lcm.metadata.api.mvnEntity.MavenPackageRefs;
import com.inspur.edp.lcm.metadata.api.service.FileService;
import com.inspur.edp.lcm.metadata.api.service.MetadataRTService;
import com.inspur.edp.lcm.metadata.cache.MdProjFileLastChangedCache;
import com.inspur.edp.lcm.metadata.common.Graph;
import com.inspur.edp.lcm.metadata.common.Utils;
import com.inspur.edp.lcm.metadata.common.Vertex;
import com.inspur.edp.lcm.metadata.common.configuration.RefStrategyHelper;
import com.inspur.edp.lcm.metadata.core.MetadataCoreManager;
import com.inspur.edp.lcm.metadata.core.MetadataProjectCoreService;
import com.inspur.edp.lcm.metadata.core.entity.MdProjDependencySource;
import com.inspur.edp.lcm.metadata.core.entity.MdProjectDependencyNode;
import com.inspur.edp.lcm.metadata.core.entity.ProjectMetadataCache;
import com.inspur.edp.metadata.rtcustomization.serverapi.CustomizationRtServerService;
import io.iec.edp.caf.commons.utils.SpringBeanUtils;
import java.io.File;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

@Slf4j
public class ProjectMetadataCacheService {
    private final static String KEY_CONNECTOR = "-";
    //原MetadataDevCacheManager中静态变量,以元数据工程路径为键存储其对应依赖 及其依赖中元数据编码列表数据
    private static final Map<String, ProjectMetadataCache> projectMetadataCacheMap = new ConcurrentHashMap<>();

    // 依赖冲突解决原则(就近\就新)
    private final String refStrategy = RefStrategyHelper.getInstance().getRefStrategy();

    private final MetadataProjectCoreService projectService = MetadataProjectCoreService.getCurrent();

    //当前元数据工程\工程路径\工作空间下 package\maven 目录
    private final MetadataProject metadataProject;
    private final String projectPath;

    // 元数据工程依赖缓存
    private final ProjectMetadataCache projectMetadataCache;

    private final MetadataCoreManager metadataManager = new MetadataCoreManager();

    private final CustomizationRtServerService customizationRtServerService;

    private final FileService fileService;

    private final MetadataRTService metadataRTService;

    /**
     * 根据元数据工程\maven路径\package路径 构造缓存类
     *
     * @param metadataProject 当前元数据工程
     * @param projectPath     当前元数据工程路径
     */
    private ProjectMetadataCacheService(MetadataProject metadataProject, String projectPath) {
        this.customizationRtServerService = SpringBeanUtils.getBean(CustomizationRtServerService.class);
        this.fileService = SpringBeanUtils.getBean(FileService.class);
        this.metadataRTService = SpringBeanUtils.getBean(MetadataRTService.class);

        this.metadataProject = metadataProject;
        this.projectPath = projectService.getProjPath(projectPath);

        // 从缓存中根据当前工程路径获取其对应的缓存内容
        if (!projectMetadataCacheMap.containsKey(projectPath)) {
            projectMetadataCacheMap.putIfAbsent(projectPath, new ProjectMetadataCache());
        }
        projectMetadataCache = projectMetadataCacheMap.get(projectPath);
    }

    /**
     * 提供公用构造方法
     *
     * @param metadataProject 当前元数据工程
     * @param projectPath     当前元数据工程路径
     * @return 构造当前工程对应的缓存类
     */
    public static ProjectMetadataCacheService getNewInstance(MetadataProject metadataProject, String projectPath) {
        return new ProjectMetadataCacheService(metadataProject, projectPath);
    }

    /**
     * 对外暴露, 获取当前元数据工程的所有依赖
     *
     * @return 当前元数据工程的所有依赖项
     */
    public Map<String, MdProjectDependencyNode> getAllReferences() {
        refreshCache();
        return projectMetadataCache.getReferences();
    }

    /**
     * 对外暴露, 计算元数据工程所有依赖
     * 在汇总的元数据编码-元数据归属工程路径 缓存列表中根据 元数据编码获取其对应的工程路径
     *
     * @param metadataId 元数据编码
     * @return 元数据归属路径
     */
    public GspMetadata getRefMetadata(String metadataId) {
        Graph<MdProjectDependencyNode> graph = refreshCache();

        // 判断是否为IDP元数据
        Set<String> idpMetadataIds = projectMetadataCache.getIdpMetadataIds();
        // 获取元数据归属依赖节点
        MdProjectDependencyNode dependencyNode = projectMetadataCache.getMetadataPackageNodes().get(metadataId);

        GspMetadata metadata = null;
        // 若元数据为IDP元数据或者归属依赖节点为运行时引用,则查询数据库获取编码对应元数据
        if (idpMetadataIds.contains(metadataId) ||
                (dependencyNode != null && dependencyNode.getSourceName() == MdProjDependencySource.DataBase)) {
            MetadataCustomizationFilter filter = new MetadataCustomizationFilter();
            filter.setMetadataId(metadataId);
            filter.setIsI18n(false);
            metadata = metadataRTService.getMetadata(filter);

        } else if (dependencyNode != null) {
            String metadataPackageLocation = dependencyNode.getLocation();
            if (StringUtils.isNotEmpty(metadataPackageLocation)) {
                File metadataPackage = new File(metadataPackageLocation);
                Metadata4Ref metadata4Ref = metadataManager.getMetadataFromPackage(metadataPackage.getName(), metadataPackage.getParent(), metadataId);
                if (metadata4Ref == null) {
                    log.error("查询元数据失败:路径[{}]元数据包中未找到元数据[{}]", metadataPackageLocation, metadataId);
                    metadataManager.throwMetadataNotFoundException(metadataId);
                    // 此时已抛出异常, 此return无意义
                    return null;
                }
                metadata = metadata4Ref.getMetadata();
            }
        }

        if (metadata == null) {
            // 若依赖图中 未查询到对应元数据,则补偿 全量查询customcontent表
            metadata = customizationRtServerService.getGenerateMetadataWithoutI18n(metadataId);
            if (metadata == null) {
                metadataManager.throwMetadataNotFoundException(metadataId);
            }
        }
        return metadata;
    }

    /**
     * 对外暴露, 计算元数据工程所有依赖
     * 在汇总的元数据编码-元数据归属工程路径 缓存列表中根据 元数据编码获取其对应的工程路径
     *
     * @param metadataId 元数据编码
     * @return 元数据归属路径
     */
    public List<GspMetadata> getRefLresMetadata(String metadataId, String resourceFileName) {
        refreshCache();
        MdProjectDependencyNode dependencyNode = projectMetadataCache.getMetadataPackageNodes().get(metadataId);

        // 若未查询到, 返回空列表
        if (dependencyNode == null) {
            return new ArrayList<>();
        }

        if (dependencyNode.getSourceName() == MdProjDependencySource.DataBase) {
            MetadataPackage metadataPackage = customizationRtServerService.findMetadataPackageByName(dependencyNode.getMetadataPackageName());
            GspMetadata gspMetadata = metadataPackage.getMetadataList().stream()
                    .filter(metadata -> metadataId.equals(metadata.getHeader().getId()))
                    .findAny().orElse(null);
            // TODO metadataPackage 数据库来源的资源元数据获取
            if (gspMetadata != null) {
                List<GspMetadata> resultList = new ArrayList<>();
                String resourceMdFileNameWithoutExt = fileService.getFileNameWithoutExtension(gspMetadata.getHeader().getFileName());
                for (GspMetadata metadata : metadataPackage.getMetadataList()) {
                    String fileName = metadata.getHeader().getFileName();
                    if (fileName.endsWith(Utils.getLanguageMetadataFileExtention())
                            && fileName.startsWith(resourceMdFileNameWithoutExt)) {

                        MetadataCustomizationFilter filter = new MetadataCustomizationFilter();
                        filter.setMetadataId(metadataId);
                        filter.setIsI18n(false);
                        GspMetadata lresMetadata = metadataRTService.getMetadata(filter);
                        resultList.add(lresMetadata);
                    }
                }
                return resultList;
            }
            return new ArrayList<>();
        }

        String metadataPackageLocation = dependencyNode.getLocation();
        // 若未查询到,返回空列表
        if (StringUtils.isEmpty(metadataPackageLocation)) {
            return new ArrayList<>();
        }

        return metadataManager.getlresMetadataFromPackage(metadataId, resourceFileName, metadataPackageLocation);
    }

    /**
     * 刷新元数据工程依赖缓存
     * 1. 重新构造依赖图
     * 2. 对比重新构造的依赖图 与缓存中的图是否一致
     * 3. 若不一致,重新计算依赖列表 并汇总其下的元数据编码列表
     */
    @SuppressWarnings("unchecked")
    private Graph<MdProjectDependencyNode> refreshCache() {
        // 首先获取缓存中元数据mdproj文件变更时间,
        LocalDateTime localDateTime = MdProjFileLastChangedCache.getLocalDateTime(projectPath);
        // 若缓存中存在, 判断缓存中 mdproj文件上次更改时间与文件时间是否一致
        File mdprojFile = projectService.getMdprojFile(projectPath);
        long lastModifyTime = mdprojFile == null? 0 : mdprojFile.lastModified();
        LocalDateTime mdprojFileLastModTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastModifyTime), ZoneId.systemDefault());
        if (localDateTime != null) {
            // 若缓存存在(缓存作为时间延迟), 且文件上次修改时间与缓存中一致, 则不再重做依赖图
            if (localDateTime.equals(mdprojFileLastModTime)) {
                return MdProjFileLastChangedCache.getGraph(projectPath);
            }
        }
        // 定义有向图
        Graph<MdProjectDependencyNode> graph = new Graph<>(true);

        // 绘制有向图，并返回根顶点（当前工程）
        Vertex<MdProjectDependencyNode> rootVertex = drawDependencyGraphWithRoot(graph, metadataProject);

        // 根据根顶点 获取最终依赖列表并构造元数据编码映射表
        refreshDependenciesCache(rootVertex);
        // 刷新缓存后, 写入缓存.
        MdProjFileLastChangedCache.put(projectPath, mdprojFileLastModTime, graph);
        return graph;
    }

    /**
     * 绘制以当前工程为根顶点的依赖图
     * @param graph 待构造的依赖图
     * @param metadataProject 当前工程
     * @return 依赖图中对应当前工程的顶点
     */
    private Vertex<MdProjectDependencyNode> drawDependencyGraphWithRoot(Graph<MdProjectDependencyNode> graph, MetadataProject metadataProject) {
        // 构造当前工程对应的节点
        MdProjectDependencyNode rootNode = MetadataPackageIndexService.getDependencyNodeByProject(projectPath, metadataProject);
        // 将当前节点入图
        graph.addRootVertex(rootNode);
        // 绘制依赖图
        drawDependencyGraph(graph, Graph.ROOT_LABEL, rootNode);
        return graph.getRootVertex();
    }

    /**
     * 根据父顶点,将其依赖元数据包构造顶点并入图
     * @param graph 依赖图
     * @param parentLable 当前父顶点标识
     * @param parentNode 父节点
     */
    private void drawDependencyGraph(Graph<MdProjectDependencyNode> graph, String parentLable, MdProjectDependencyNode parentNode) {
        // 复制节点中记录的所有元数据包引用信息
        Set<String> metadataPackageRefs = new HashSet<>(parentNode.getMetadataPackageRefs());
        // 记录bo内引用的元数据包引用信息
        Set<String> metadataPackageFromBO = new HashSet<>();

        // 根据bo内引用 构造顶点并与当前顶点连接
        generateBoVertex(graph, parentLable, parentNode, metadataPackageRefs, metadataPackageFromBO);
        // 根据数据库引用 构造对应顶点并与当前顶点连接
        generateDBVertex(graph, parentLable, parentNode, metadataPackageRefs, metadataPackageFromBO);
        // 根据Maven引用 构造对应顶点并与当前顶点连接
        generateMavenVertex(graph, parentLable, parentNode, metadataPackageRefs, metadataPackageFromBO);
        // 根据Package引用 构造对应顶点并与当前顶点连接
        generatePackageVertex(graph, parentLable, metadataPackageRefs);
    }

    private void generateBoVertex(Graph<MdProjectDependencyNode> graph,
                                  String parentLable, MdProjectDependencyNode parentNode,
                                  Set<String> metadataPackageRefs, Set<String> metadataPackageFromBO) {
        //遍历所有bo内引用
        for (ProjectHeader projectRepoKey : parentNode.getProjectRefs()) {
            String metadataPackageName = projectRepoKey.getName();
            // 若元数据包引用中不包含此bo内引用,不做处理
            if (!metadataPackageRefs.contains(metadataPackageName)) {
                // TODO 异常场景完善
                continue;
            }
            // 此元数据引用认为是bo内引用
            metadataPackageRefs.remove(metadataPackageName);
            metadataPackageFromBO.add(metadataPackageName);

            // 图中以label作为唯一标识
            String label = generateLabelForProject(projectRepoKey);
            // 判断图中是否已存在此顶点
            Vertex<MdProjectDependencyNode> vertex = graph.getVertex(label);
            if (vertex == null) {
                // 若图中无此节点,则构造
                String refProjectPath = projectPath + File.separator + projectRepoKey.getProjectPath();
                MdProjectDependencyNode projectRefNode = MetadataPackageIndexService.getDependencyNodeByProject(refProjectPath);
                // 根据路径无法获取对应的bo内依赖元数据工程, 不做处理
                // TODO 异常场景完善
                if (projectRefNode == null) {
                    continue;
                }
                // 依赖节点入图,并构造与父顶点关系
                graph.addVertex(label, projectRefNode);
                graph.addEdge(parentLable, label);
                // 递归绘制子节点
                drawDependencyGraph(graph, label, projectRefNode);
            } else {
                // 若图中已存在,则直接使用此顶点
                graph.addEdge(parentLable, label);
            }
        }
    }

    private void generateDBVertex(Graph<MdProjectDependencyNode> graph,
                                  String parentLable, MdProjectDependencyNode parentNode,
                                  Set<String> metadataPackageRefs, Set<String> metadataPackageFromBO) {
        Set<String> mdpkgRefs = parentNode.getMdpkgRefs();
        //遍历所有数据库引用
        for (String mdpkgRefName : mdpkgRefs) {
            // 若存在元数据包的bo内引用,则不做处理
            if (metadataPackageFromBO.contains(mdpkgRefName)) {
                continue;
            }
            // 去除元数据包的其他引用形式
            metadataPackageRefs.remove(mdpkgRefName);
            // 图中以label作为唯一标识
            String label = generateLabelForDB(mdpkgRefName);
            // 判断图中是否已存在此顶点
            Vertex<MdProjectDependencyNode> vertex = graph.getVertex(label);
            if (vertex == null) {
                // 若图中无此节点,则构造
                MdProjectDependencyNode mdpkgRefNode = MetadataPackageIndexService.getDependencyNodeByDB(mdpkgRefName);
                // 根据路径无法获取对应的bo内依赖元数据工程, 不做处理
                // TODO 异常场景完善
                if (mdpkgRefNode == null) {
                    continue;
                }
                // 依赖节点入图,并构造与父顶点关系
                graph.addVertex(label, mdpkgRefNode);
                graph.addEdge(parentLable, label);
                // 递归绘制子节点
                drawDependencyGraph(graph, label, mdpkgRefNode);
            } else {
                // 若图中已存在,则直接使用此顶点
                graph.addEdge(parentLable, label);
            }
        }
    }

    private void generateMavenVertex(Graph<MdProjectDependencyNode> graph,
                                     String parentLable, MdProjectDependencyNode parentNode,
                                     Set<String> metadataPackageRefs, Set<String> metadataPackageFromBO) {
        // 遍历所有Maven引用
        for (MavenPackageRefs mavenRepoKey : parentNode.getMavenRefs()) {
            // 构造顶点唯一标识
            String label = generateLabelForMaven(mavenRepoKey);
            // 查看图中是否已存在此顶点
            Vertex<MdProjectDependencyNode> vertex = graph.getVertex(label);
            MdProjectDependencyNode mavenNode;
            if (vertex == null) {
                // 若图中无此节点,则构造
                mavenNode = MetadataPackageIndexService.getDependencyNodeByMaven(mavenRepoKey);
                // 根据路径无法获取对应的bo内依赖元数据工程, 不做处理
                // TODO 异常场景完善
                if (mavenNode == null) {
                    continue;
                }

                // 若同时存在maven引用途径和bo引用途径，不考虑maven引用途径
                if (metadataPackageFromBO.contains(mavenNode.getMetadataPackageName())) {
                    continue;
                }

                graph.addVertex(label, mavenNode);
                graph.addEdge(parentLable, label);
                drawDependencyGraph(graph, label, mavenNode);
            } else {
                mavenNode = vertex.getContent();
                // 若同时存在maven引用途径和bo引用途径，不考虑maven引用途径
                if (metadataPackageFromBO.contains(mavenNode.getMetadataPackageName())) {
                    continue;
                }

                graph.addEdge(parentLable, label);
            }
            metadataPackageRefs.remove(mavenNode.getMetadataPackageName());
        }
    }

    private void generatePackageVertex(Graph<MdProjectDependencyNode> graph, String parentLable, Set<String> metadataPackageRefs) {
        // 剩下的均视为Package引用
        for (String metadataPackageRefName : metadataPackageRefs) {
            String label = generateLabelForPackage(metadataPackageRefName);
            Vertex<MdProjectDependencyNode> vertex = graph.getVertex(label);
            if (vertex == null) {
                // 若图中无此节点,则构造
                MdProjectDependencyNode packageNode = MetadataPackageIndexService.getDependencyNodeByPackage(metadataPackageRefName);
                // 根据路径无法获取对应的bo内依赖元数据工程, 不做处理
                // TODO 异常场景完善
                if (packageNode == null) {
                    continue;
                }
                graph.addVertex(label, packageNode);
                graph.addEdge(parentLable, label);
                drawDependencyGraph(graph, label, packageNode);
            } else {
                graph.addEdge(parentLable, label);
            }
        }
    }

    /**
     * 1. 通过依赖图, 确定最终依赖列表
     * 2. 遍历依赖列表获取 汇总依赖下元数据编码列表
     *
     * @author sunhongfe01
     * @param rootVertex 当前依赖有向图中根顶点
     */
    private void refreshDependenciesCache(Vertex<MdProjectDependencyNode> rootVertex) {
        Map<String, MdProjectDependencyNode> allDependencies;
        allDependencies = getAllDependenciesByStrategy(rootVertex);
        projectMetadataCache.setReferences(allDependencies);

        Map<String, MdProjectDependencyNode> metadataPackageLocations = projectMetadataCache.getMetadataPackageNodes();
        Set<String> idpMetadataIds = projectMetadataCache.getIdpMetadataIds();

        metadataPackageLocations.clear();
        idpMetadataIds.clear();
        for (MdProjectDependencyNode dependencyNode : allDependencies.values()) {
            // 仅收集非BO引用节点中的元数据id
            if (!dependencyNode.isBO()) {
                dependencyNode.getMetadataIds()
                        .forEach(metadataId -> metadataPackageLocations.put(metadataId, dependencyNode));
            }
            // 收集所有节点的 idp元数据引用
            idpMetadataIds.addAll(dependencyNode.getIdpMetadataIds());
        }


    }

    /**
     * 根据处理原则(就近 就新) 使用宽搜方式处理依赖图,获得最终依赖列表
     * 若为就近,则宽搜时 以先处理版本为准, 较远依赖节点 及其子节点舍弃.
     * 若为就新, 则全量遍历, 依赖选用 最新版本依赖节点
     *
     * @param vertex 获取最终依赖列表使用跟顶点
     * @author sunhongfe01
     */
    private Map<String, MdProjectDependencyNode> getAllDependenciesByStrategy(Vertex<MdProjectDependencyNode> vertex) {
        Map<String, MdProjectDependencyNode> allDependencies = new HashMap<>();

        // 当前顶点标记为已处理后推入队列中
        Queue<Vertex<MdProjectDependencyNode>> queue = new LinkedList<>();
        vertex.visit();
        queue.add(vertex);

        // 获取就近就新策略
        boolean isNearest = StringUtils.equals(refStrategy, "nearest");

        while (!queue.isEmpty()) {
            Vertex<MdProjectDependencyNode> curVertex = queue.poll();
            if (curVertex == null || curVertex.getContent() == null) {
                continue;
            }
            MdProjectDependencyNode curNode = curVertex.getContent();
            List<Vertex<MdProjectDependencyNode>> unvisitChildren = curVertex.getChildren()
                    .stream().filter(Vertex::isNotVisited).collect(Collectors.toList());
            unvisitChildren.forEach(Vertex::visit);
            // BO内引用
            if (curNode.isBO()) {
                // BO引用写入最终依赖列表中
                String boNodeKey = curNode.getSourceName().name() + KEY_CONNECTOR + curNode.getMetadataPackageName();
                allDependencies.put(boNodeKey, curNode);
                // 过滤掉已处理的顶点

                queue.addAll(unvisitChildren);
                continue;
            }
            String curKey = curNode.getMetadataPackageName();
            if (allDependencies.containsKey(curKey)) {
                // 若为就近,则后引入依赖舍弃
                // 若为就新,则判断版本(BO/package不存在版本), 若已采用的依赖版本大于等于当前节点,则不使用当前节点
                if (!isNearest) {
                    MdProjectDependencyNode dependencyNode = allDependencies.get(curKey);

                    if (curNode.isVersionNewerThan(dependencyNode, false)) {
                        allDependencies.put(curKey, curNode);
                        // 过滤掉已处理的顶点
                        queue.addAll(unvisitChildren);
                    }
                }
                continue;
            }
            allDependencies.put(curKey, curNode);
            // 过滤掉已处理的顶点
            queue.addAll(unvisitChildren);
        }
        return allDependencies;
    }

    /**
     * BO内引用，构造其在图中唯一标识
     * @param projectRepoKey bo内引用信息
     * @return 图中唯一标识
     */
    private String generateLabelForProject(ProjectHeader projectRepoKey) {
        return projectRepoKey.getProjectPath();
    }

    /**
     * 数据库引用,构建在图中的唯一标识
     * @param mdpkgRefName 数据库引用元数据包名
     * @return 图中唯一标识
     */
    private String generateLabelForDB(String mdpkgRefName) {
        return StringUtils.join("DB" ,KEY_CONNECTOR, mdpkgRefName);
    }

    /**
     * Maven引用，构造其在图中唯一标识
     * @param mavenRepoKey Maven引用信息
     * @return 图中唯一标识
     */
    private String generateLabelForMaven(MavenPackageRefs mavenRepoKey) {
        return StringUtils.join(mavenRepoKey.getGroupId(),KEY_CONNECTOR,
                mavenRepoKey.getArtifactId(), KEY_CONNECTOR, mavenRepoKey.getVersion());
    }

    /**
     * Package引用，构造其在图中唯一标识
     * @param metadataPackageRefName 元数据包名
     * @return 图中唯一标识
     */
    private String generateLabelForPackage(String metadataPackageRefName) {
        return metadataPackageRefName;
    }
}